﻿////////////////////////////////////////////////////////////////////////////////////
//
// Copyright (c) 2008 Piers Lawson
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in the
// Software without restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
// Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
// AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
////////////////////////////////////////////////////////////////////////////////////

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Mime;
using System.Web.Mvc;
using System.Web;
using System.Globalization;

namespace ResourcesOverMvc.Web.Mvc
{
    /// <summary>
    /// An Action Filer attribute that replaces the default ActionResult created by a controller with
    /// one that is able to create a response of the correct Content-Type
    /// </summary>
    public class MultipleRepresentationsAttribute : ActionFilterAttribute
    {
        // Helper that can identify if the attribute is prenet on a Controller class
        public static bool IsDefined(ControllerBase controller)
        {
            Type controllerType = controller.GetType();
            var restEnabledAttribute = controllerType.GetCustomAttributes(typeof(MultipleRepresentationsAttribute), true) as MultipleRepresentationsAttribute[];
            return restEnabledAttribute != null && restEnabledAttribute.Length > 0;
        }

        // Replaces the ActionResult created by the controller with one able to
        // render Content-Type acceptable to the client
        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            var viewResult = filterContext.Result as ViewResultBase;

            if (viewResult != null && viewResult.ViewData != null)
            {
                ActionResult replacementResult = null;

                // Create a list of the charsets the client is willing to support in order of preference
                // (adding the encoding the client used in the request as the last alternative)
                var charsetList = new CharsetList(filterContext.HttpContext.Request.Headers["Accept-Charset"])
                                      {new Charset(filterContext.HttpContext.Request.ContentEncoding, 0.0001F)};

                // Sort the accept types acceptable to the client into order of preference then look for
                // a response handler that supports one of the accept types.
                foreach (var contentTypeWrapper in GetAcceptHeaderContentTypes(filterContext.HttpContext.Request.AcceptTypes))
                {
                    replacementResult = FormatManager.Current.TryCreateActionResult(viewResult.ViewName, viewResult.ViewData.Model, contentTypeWrapper.ContentType, charsetList);

                    if (replacementResult != null)
                    {
                        break;
                    }
                }

                if (replacementResult == null)
                {
                    throw new HttpException((int)HttpStatusCode.NotAcceptable, "None of the formats specified in the accept header is supported.");
                }

                // Interogate the controller to see if it wants to dictate the verbs allowed to be
                // used against the current resource.
                var iAllowedVerbs = filterContext.Controller as IAllowedVerbs;
                if (iAllowedVerbs != null)
                {
                    string allowedVerbs = iAllowedVerbs.GetAllowedVerbs(filterContext);
                    if (!string.IsNullOrEmpty(allowedVerbs))
                    {
                        filterContext.HttpContext.Response.AppendHeader("Allow", allowedVerbs);
                    }
                }

                filterContext.Result = replacementResult;
            }

            base.OnActionExecuted(filterContext);
        }

        #region Sorting of accept types

        internal static List<ContentTypeWrapper> GetAcceptHeaderContentTypes(string[] acceptHeaderElements)
        {
            var contentTypeWraperList = new List<ContentTypeWrapper>(acceptHeaderElements.Length);
            int index = 0;
            foreach (string acceptHeaderElement in acceptHeaderElements)
            {
                try
                {
                    var contentTypeWrapper = new ContentTypeWrapper(new ContentType(acceptHeaderElement), index);
                    contentTypeWraperList.Add(contentTypeWrapper);
                }
                catch (FormatException)
                {
                    // Ignore unknown formats to allow fallback
                }
                index++;
            }
            contentTypeWraperList.Sort(new AcceptHeaderElementComparer());
            return contentTypeWraperList;
        }

        internal class ContentTypeWrapper
        {
            public ContentType ContentType { get; set; }
            public int OriginalIndex { get; set; }

            public ContentTypeWrapper(ContentType contentType, int originalPosition)
            {
                ContentType = contentType;
                OriginalIndex = originalPosition;
            }
        }

        /// <summary>
        /// Can be used to sort a list of accept types into an order of preference. The "quality",
        /// specificity and original order are taken into account.
        /// </summary>
        class AcceptHeaderElementComparer : IComparer<ContentTypeWrapper>
        {
            public int Compare(ContentTypeWrapper x, ContentTypeWrapper y)
            {
                // Quality Factor is the over riding control of order
                var qualityDifference = GetQualityFactor(x.ContentType) - GetQualityFactor(y.ContentType);
                if (qualityDifference < 0)
                {
                    return 1;
                }
                if (qualityDifference > 0)
                {
                    return -1;
                }

                // Next control is which is the more specific
                string[] xTypeSubType = x.ContentType.MediaType.Split('/');
                string[] yTypeSubType = y.ContentType.MediaType.Split('/');

                if (string.Equals(xTypeSubType[0], yTypeSubType[0], StringComparison.OrdinalIgnoreCase))
                {
                    if (string.Equals(xTypeSubType[1], yTypeSubType[1], StringComparison.OrdinalIgnoreCase))
                    {
                        // need to check the number of parameters to determine which is more specific
                        int xNonQParameterCount = NonQParameterCount(x.ContentType);
                        int yNonQParameterCount = NonQParameterCount(y.ContentType);
                        if (xNonQParameterCount < yNonQParameterCount)
                        {
                            return 1;
                        }
                        if (xNonQParameterCount > yNonQParameterCount)
                        {
                            return -1;
                        }
                    }
                    else
                    {
                        if (xTypeSubType[1][0] == '*' && xTypeSubType[1].Length == 1)
                        {
                            return 1;
                        }
                        if (yTypeSubType[1][0] == '*' && yTypeSubType[1].Length == 1)
                        {
                            return -1;
                        }
                    }
                }

                // Lastly, if we still cannot distinguish we will assume the earliest in the list 
                // is the preferred type
                if (x.OriginalIndex > y.OriginalIndex)
                {
                    return 1;
                }
                if (x.OriginalIndex < y.OriginalIndex)
                {
                    return -1;
                }

                return 0;
            }

            static decimal GetQualityFactor(ContentType contentType)
            {
                if (contentType.Parameters != null)
                {
                    foreach (string key in contentType.Parameters.Keys.Cast<string>().Where(key => string.Equals("q", key, StringComparison.OrdinalIgnoreCase)))
                    {
                        decimal result;
                        if (decimal.TryParse(contentType.Parameters[key], NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out result) && (result <= (decimal) 1.0))
                        {
                            return result;
                        }
                    }
                }

                return (decimal)1.0;
            }

            static int NonQParameterCount(ContentType contentType)
            {
                var number = 0;
                if (contentType.Parameters != null)
                {
                    number = contentType.Parameters.Keys.Cast<string>().Count(param => !string.Equals("q", param, StringComparison.OrdinalIgnoreCase));
                }

                return number;
            }
        }

        #endregion
    }
}
